/* Copyright 2006 david reid <mail@david-reid.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "httpd.h"
#include "http_config.h"
#include "http_core.h"
#include "http_log.h"
#include "http_main.h"
#include "http_protocol.h"
#include "http_request.h"
#include "util_script.h"
#include "http_connection.h"

#include "apr_lib.h"
#include "apr_strings.h"
#include "apr_md5.h"
#include "apr_uri.h"
#include "apr_strmatch.h"


module AP_MODULE_DECLARE_DATA session_module;

#define SESSION_VER_MAJOR        0
#define SESSION_VER_MINOR        1
#define BROWSER_EXEMPT           "nosessioncontrol"
#define MAX_COOKIE_NAME_LENGTH   128 /* what should this be? */

typedef struct session_data {
    apr_pool_t *pool;         /* pool we allocate memory from */
    char *path;               /* path session data applies to */

    int enabled;              /* are session keys enabled? */

    char *cookie_name;        /* cookie name to use */
    char *cookie_domain;      /* domain path for cookie */
    char *cookie_path;        /* path cookie applies to */

    apr_int64_t expiration;   /* no of secs the cookie is valid */

} session_data_t;

typedef struct session_key {
    session_data_t *data;
    char *key;
    char *remote;
    char *hostname;
    char *path;
    apr_time_t expires;
} session_key_t;

static const char *session_enable(cmd_parms *cmd, void *config, int mode)
{
    session_data_t *sd = (session_data_t *)config;
    sd->enabled = mode;

/*
    if (sd->enabled)
        ap_log_error(APLOG_MARK, APLOG_INFO, 0, NULL,
                     "Session keys enabled!");
*/
    return(NULL);
}

static const char *session_set_cookie_name(cmd_parms *cmd, void *config,
                                           const char *mode)
{
    session_data_t *sd = (session_data_t *)config;

    if (strlen(mode) > MAX_COOKIE_NAME_LENGTH) {
        ap_log_error(APLOG_MARK, APLOG_INFO, 0, cmd->server,
                     "Cookie names cannot exceed %d charatcters, truncating",
                     MAX_COOKIE_NAME_LENGTH);
    }
    sd->cookie_name = apr_pstrndup(sd->pool, mode, MAX_COOKIE_NAME_LENGTH);

    return NULL;
}

/* Cleanup the session. This is called when we destroy the 
 * session_data_t object.
 * This is mainly just a place holder to add any cleanups that
 * may be required. Presently there arent any.
 */
static apr_status_t session_cleanup(void *dummy)
{
    session_data_t *sd = (session_data_t *)dummy;

    /* Insert cleanups here */

    return APR_SUCCESS;
}

/* Add the module to the component description line.
 * Any additional startup tasks should be added here.
 */
static int session_init_handler(apr_pool_t *p, apr_pool_t *plog, 
                                apr_pool_t *ptemp, server_rec *s)
{
    char *verString = NULL;

    verString = apr_psprintf(ptemp, "SessionHandler %d.%d",
                             SESSION_VER_MAJOR,
                             SESSION_VER_MINOR);

    ap_add_version_component(p, verString);

    /* Insert startup tasks here */

    return OK;
}

/* This function creates the session key and sets it as a cookie
 * for the client. There is no return value.
 *
 * The key is an md5 hash of 
 *   - the remote hostname
 *   - the local hostname
 *   - the path
 *   - the expiration time of the key
 *
 */
static void sessionKeyCreate(request_rec *r, session_data_t *sd)
{
    apr_md5_ctx_t md5;
    char digest[APR_MD5_DIGESTSIZE];
    char digestStr[(APR_MD5_DIGESTSIZE * 2) + 1];
    char dateStr[APR_RFC822_DATE_LEN];
    char *hostname = NULL, *remote = NULL;
    char *cookie = NULL;
    apr_time_t now;
    apr_time_exp_t expTime;
    apr_size_t dsLength = 0;
    int is_ip = 0, i;

    now = apr_time_now();
    now += sd->expiration * APR_USEC_PER_SEC;
    apr_time_exp_gmt(&expTime, now);
    remote = apr_pstrdup(sd->pool, ap_get_remote_host(r->connection,
                                                      r->per_dir_config,
                                                      REMOTE_NAME,
                                                      &is_ip));
    hostname = apr_pstrdup(sd->pool, r->hostname);
    apr_strftime(dateStr, &dsLength, APR_RFC822_DATE_LEN,
                 "%a, %d-%b-%Y %R:%S GMT", &expTime);

    if (apr_md5_init(&md5) == APR_SUCCESS) {
        if (remote)
            apr_md5_update(&md5, remote, strlen(remote));
        if (hostname)
            apr_md5_update(&md5, hostname, strlen(hostname));
        apr_md5_update(&md5, sd->path, strlen(sd->path));
        apr_md5_update(&md5, dateStr, strlen(dateStr));    
        apr_md5_final((unsigned char *)digest, &md5);
    }

    for (i = 0; i < APR_MD5_DIGESTSIZE; i++)
        sprintf(&digestStr[i * 2], "%02x", digest[i] & 0xff);

    apr_table_set(r->subprocess_env, "SESSION_KEY", digestStr);
    apr_table_set(r->subprocess_env, "SESSION_KEY_NAME", sd->cookie_name);
    apr_table_set(r->notes, "SESSION_KEY", digestStr);
    apr_table_set(r->notes, "SESSION_KEY_NAME", sd->cookie_name);

    if (!sd->cookie_domain)
        cookie = apr_psprintf(sd->pool, "%s=%s; expires=%s; path=%s",
                              sd->cookie_name, digestStr, dateStr,
                              sd->path);
    else
        cookie = apr_psprintf(sd->pool, 
                              "%s=%s; domain=%s; expires=%s; path=%s",
                              sd->cookie_name, digestStr, 
                              sd->cookie_domain,
                              dateStr, sd->path);

    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "cookie '%s'", cookie);

    apr_table_merge(r->headers_out, "Set-Cookie", cookie);
}

static void checkHeaderTable(apr_table_t *in, apr_table_t *out, 
                             const char *which)
{
    if (apr_table_get(in, which))
        apr_table_set(out, which, apr_table_get(in, which));
}

static int checkRequests(request_rec *r, request_rec *rr)
{

#define SET_VAR(a, b, var)                              \
    apr_table_set(b->subprocess_env, var,               \
                 apr_table_get(a->subprocess_env, var))
#define SET_NOTE(a, b, var)                             \
    apr_table_set(b->notes, var, apr_table_get(a->notes, var))

    if (apr_table_get(r->subprocess_env, "SESSION_KEY")) {
        SET_VAR(r, rr, "SESSION_KEY");
        SET_VAR(r, rr, "SESSION_KEY_NAME");
        SET_NOTE(r, rr, "SESSION_KEY");
        SET_NOTE(r, rr, "SESSION_KEY_NAME");
        return 1;
    }
    return 0;
}

/* There are 3 places that we can find a session key
 *
 *   1) in the subprocess_env of a parent request
 *   2) in r->main of a parent request
 *   3) in a cookie header (r->headers_in)
 */
static int session_detect_key(request_rec *r, session_data_t *sd)
{
    request_rec *rp = r->prev;
    const char *str = NULL;

    /* Case 1 */
    while (rp) {
        checkHeaderTable(rp->headers_out, r->headers_out, "Set-Cookie");
        if (checkRequests(rp, r))
            return 1;
        rp = rp->prev;
    }

    /* Case 2 */
    if (r->main) {
        checkHeaderTable(r->main->headers_out, r->headers_out, 
                         "Set-Cookie");
        if (checkRequests(r->main, r))
            return 1;
    }

    /* Case 3 */
    str = apr_table_get(r->headers_in, "Cookie");
    if (str) {
        char *val, *ptr;
        char *buffer = apr_psprintf(sd->pool, "%s=", sd->cookie_name);

        if ((val = strstr(str, buffer))) {
            val += strlen(buffer);
            for (ptr = val + 1; *ptr != ';' && *ptr != '\0'; ptr++)
                ;
            buffer = apr_pstrndup(sd->pool, val, ptr - val);

            apr_table_set(r->subprocess_env, "SESSION_KEY", buffer);
            apr_table_set(r->subprocess_env, "SESSION_KEY_NAME",
                         sd->cookie_name);

            apr_table_set(r->notes, "SESSION_KEY", buffer);
            apr_table_set(r->notes, "SESSION_KEY_NAME", sd->cookie_name);

            return 1;
        }
    }

    return 0;
}

/* This fucntion does all the hard work. It's called to check if we
 * have a session key and make sure it's correctly set so that other
 * modules can find and use it.
 * If the key hasn't been set then it is set here.
 */
static int session_fixups(request_rec *r)
{
    session_data_t *sd = NULL;

    if (apr_table_get(r->subprocess_env, BROWSER_EXEMPT))
        return DECLINED;

    /* If we're a subrequest we need to check to see if one of our
     * elders has a session key already. If it does we'll use the config
     * from that key.
     */
    if (r->prev) {
        request_rec *rp = r->prev;
        while (rp) {
            if (apr_table_get(rp->subprocess_env, "SESSION_KEY") &&
                apr_table_get(rp->subprocess_env, "SESSION_KEY_METHOD")) {
                sd = (session_data_t *)ap_get_module_config(rp->per_dir_config,
                                                            &session_module);
                break;
            }
            rp = rp->prev;
        }
    }

    /* If we didn't find a session key in the parent, check to see if we
     * have one in the headers for this request.
     */
    if (!sd) {
        sd = (session_data_t *)ap_get_module_config(r->per_dir_config,
                                                    &session_module);

        if (! sd->enabled) {
            ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
                          "Session Keys not enabled for '%s', declining",
                          r->uri);
            return DECLINED;
        }

        if (!sd->path ||
            strncmp(r->uri, sd->path, strlen(sd->path))) {
            ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
                          "Session data: Incorrect path!");
            return DECLINED;
        }
    }

    if (!session_detect_key(r, sd))
        sessionKeyCreate(r, sd);
    else
        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
                      "cookie detected!!!");

    return OK;
}

/* Create a new session_data_t structure with the defaults correctly
 * set.
 */
static session_data_t *makeSessionData(apr_pool_t *p)
{
    session_data_t *sd = NULL;

    sd = (session_data_t *)apr_pcalloc(p, sizeof(*sd));
    if (!sd)
        return NULL;

    sd->pool = p;
    sd->cookie_name = "sessionId";
    sd->expiration = 86400;         /* seconds */

    apr_pool_cleanup_register(p, sd, session_cleanup, 
                              apr_pool_cleanup_null);
    return sd;
}

/* When we set sessions On for an entire server we assume the path
 * is just '/'.
 */
static void *session_create_config(apr_pool_t *p, server_rec *s)
{
    session_data_t *sd = makeSessionData(p);
    if (!sd)
        return NULL;
    sd->path = "/";
    return sd;
}

/* When enabled for a directory, we set the path to the directory we
 * are being passed.
 * NB when used via a Directory config directive we end up with the full
 *    path, so don't do that! Use Location instead to get the correct
 *    URI path.
 */
static void *session_dir_config(apr_pool_t *p, char *dir)
{
    session_data_t *sd = makeSessionData(p);
    if (!sd)
        return NULL;
    sd->path = apr_pstrdup(sd->pool, dir);
    return sd;
}

static const command_rec session_cmds[] =
{
    AP_INIT_FLAG("Session", session_enable, NULL, RSRC_CONF|ACCESS_CONF,
                  "Use session keys [default No]"),
    AP_INIT_TAKE1("SessionCookieName", session_set_cookie_name, NULL, 
                  RSRC_CONF|ACCESS_CONF,
                  "Set the name to use for cookies [default is 'session']"),
    AP_INIT_TAKE1("SessionExpiration", ap_set_int_slot,
                  (void *)APR_OFFSETOF(session_data_t, expiration),
                  RSRC_CONF|ACCESS_CONF,
                  "Expiration time of created session (in seconds)"
                  " [default 3600]"),
    AP_INIT_TAKE1("SessionDomain", ap_set_string_slot,
                  (void *)APR_OFFSETOF(session_data_t, cookie_domain),
                  RSRC_CONF|ACCESS_CONF,
                  "Domain the cookie should be set for"),
    {NULL}
};

static void register_hooks(apr_pool_t *p)
{
    ap_hook_post_config(session_init_handler, NULL, NULL, APR_HOOK_MIDDLE);
    ap_hook_fixups(session_fixups, NULL, NULL, APR_HOOK_MIDDLE);
}

module AP_MODULE_DECLARE_DATA session_module =
{
    STANDARD20_MODULE_STUFF,
    session_dir_config,        /* create per-directory config structure */
    NULL,                      /* merge per-directory config structures */
    session_create_config,     /* create per-server config structure */
    NULL,                      /* merge per-server config structures */
    session_cmds,              /* command apr_table_t */
    register_hooks
};
